package com.zoe.onelink.core.exception.handler;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import com.zoe.onelink.common.constant.OnelinkConstant;
import com.zoe.onelink.common.entity.ResultVO;
import com.zoe.onelink.common.enumeration.ExceptionCode;
import com.zoe.onelink.common.enumeration.ExceptionCodeEnum;
import com.zoe.onelink.common.enumeration.OnelinkExceptionCode;
import com.zoe.onelink.common.exception.ApiException;
import com.zoe.onelink.common.exception.InnerApiException;
import com.zoe.onelink.common.exception.PermissionException;
import com.zoe.onelink.common.exception.http.HttpClientCallTimeoutException;
import com.zoe.onelink.common.exception.http.HttpClientConnectTimeoutException;
import com.zoe.onelink.common.exception.http.HttpClientException;
import com.zoe.onelink.common.model.IResult;
import com.zoe.onelink.core.exception.ExceptionResolver;
import com.zoe.onelink.core.exception.guide.OnelinkExceptionGuideProvider;
import com.zoe.onelink.core.exception.interceptor.WebExceptionInterceptor;
import com.zoe.onelink.core.property.OnelinkProperties;
import com.zoe.onelink.core.util.LocaleResolverUtil;
import com.zoe.onelink.core.validation.ValidationFieldInfo;
import com.zoe.onelink.core.web.validation.resolver.ValidationFieldNameResolver;
import com.zoe.onelink.util.OnelinkContextHolder;
import com.zoe.onelink.util.StackTraceUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * <p>标题: </p>
 * <p>描述: </p>
 * <p>版权: Copyright (c) 2021</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2021-07-28
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * Core参数
     */
    private final OnelinkProperties onelinkProperties;

    /**
     * 异常拦截器
     */
    private final List<WebExceptionInterceptor> interceptors;

    /**
     * 异常解析器
     */
    private final List<ExceptionResolver> exceptionResolvers;

    /**
     * 异常返回结果处理器
     */
    private final List<OnelinkExceptionGuideProvider> onelinkExceptionGuideProviders;

    /**
     * 请求参数验证字段名解析器
     */
    private final ValidationFieldNameResolver validationFieldNameResolver;

    /**
     * SQL注入
     */
    public static final String SQL_INJECTION_EX_PREFIX = "sql injection";

    /**
     * ORACLE 数据库异常开头
     */
    public static final String ORACLE_EXCEPTION_PREFIX = "ORA-";

    /**
     * Lombok非空参数验证提示后缀
     */
    private static final String LOMBOK_NONNULL_VALIDATION_MESSAGE_SUFFIX = "is marked non-null but is null";

    private final HttpServletResponse response;

    private final String applicationName;

    @Value("${onelink.guide.enabled:false}")
    private boolean enableExceptionGuide;

    public GlobalExceptionHandler(OnelinkProperties onelinkProperties,
                                  ObjectProvider<List<WebExceptionInterceptor>> webExceptionInterceptors,
                                  List<ExceptionResolver> exceptionResolvers,
                                  List<OnelinkExceptionGuideProvider> onelinkExceptionGuideProviders, ObjectProvider<ValidationFieldNameResolver> validationFieldNameResolver,
                                  HttpServletResponse response,
                                  Environment environment) {
        this.onelinkProperties = onelinkProperties;
        this.interceptors = webExceptionInterceptors.getIfAvailable();
        this.exceptionResolvers = exceptionResolvers;
        this.onelinkExceptionGuideProviders = onelinkExceptionGuideProviders;
        this.validationFieldNameResolver = validationFieldNameResolver.getIfAvailable();
        this.response = response;
        this.applicationName = environment.getProperty("spring.application.name", "unknown");
    }

    /**
     * 异常处理总入口 , 这里不使用多个@ExceptionHandler分开处理,由该入口统一分发,然后对异常结果进行统一处理
     */
    @ExceptionHandler(Exception.class)
    @SneakyThrows
    public Object exceptionHandler(Exception ex) {
        IResult<?> result;
        try {
            String traceId = OnelinkContextHolder.getString(OnelinkConstant.TRACE_ID);
            // 响应头增加链路ID
            this.response.setHeader(OnelinkConstant.TRACE_ID, StrUtil.nullToEmpty(traceId));
            // 先默认设置HTTP状态码为500,然后根据具体异常处理再调整对应的状态码
            this.response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            // 统一分发并处理异常
            result = this.handleException(ex);
        } catch (Exception e) {
            result = ResultVO.failure(ex.getMessage(), ExceptionUtil.stacktraceToString(e));
        }

        // 是否开启异常处理指南
        if (this.enableExceptionGuide) {
            this.appendExGuide(ex, result);
        }
        return result;
    }

    /**
     * 添加异常处理指南信息
     */
    private void appendExGuide(Throwable ex, final IResult<?> result) {
        if (CollUtil.isEmpty(this.onelinkExceptionGuideProviders) || !(result instanceof ResultVO)) {
            return;
        }
        ResultVO<?> resultVO = (ResultVO<?>) result;
        // 解析当前异常是否包含API异常
        ApiException apiException = this.resolveApiException(ex);
        // API异常优先
        Throwable resolveEx = apiException == null ? ex : apiException;

        // 遍历所有实现
        for (OnelinkExceptionGuideProvider onelinkExceptionGuideProvider : this.onelinkExceptionGuideProviders) {
            // 判断是否支持当前异常类
            if (!onelinkExceptionGuideProvider.supports(resolveEx, result)) {
                continue;
            }
            // 获取异常指南信息
            String guide = this.getExceptionGuide(result, resolveEx, onelinkExceptionGuideProvider);
            if (StrUtil.isNotBlank(guide)) {
                resultVO.setGuide(guide);
                return;
            }

        }
    }

    /**
     * 获取异常处理指南
     */
    private String getExceptionGuide(IResult<?> result, Throwable resolveEx, OnelinkExceptionGuideProvider onelinkExceptionGuideProvider) {
        if (onelinkExceptionGuideProvider == null) {
            return null;
        }
        try {
            return onelinkExceptionGuideProvider.getGuide(resolveEx, result);
        } catch (Exception e) {
            return null;
        }
    }

    private IResult<?> handleException(Exception ex) {
        ResultVO<Object> resultVO;
        // 异常拦截器
        if (this.interceptors != null) {
            for (WebExceptionInterceptor interceptor : this.interceptors) {
                ex = interceptor.beforeHandle(ex);
            }
        }
        // 根据异常类型进行处理
        resultVO = this.dispatchException(ex);
        // 优化ResultVO数据
        this.optimizeResultVO(ex, resultVO);
        // 异常拦截器回调
        if (this.interceptors != null) {
            for (WebExceptionInterceptor interceptor : this.interceptors) {
                ex = interceptor.afterHandle(ex, resultVO);
            }
        }
        // 业务侧异常码使用警告级别日志
        if (ex instanceof ApiException && !(((ApiException) ex).getExceptionCode() instanceof OnelinkExceptionCode)) {
            log.warn(resultVO.getMessage(), ex);
        } else {
            log.error(resultVO.getMessage(), ex);
        }
        return resultVO;
    }

    /**
     * 优化ResultVO数据
     */
    private void optimizeResultVO(Exception ex, ResultVO<Object> resultVO) {
        // 获取当前异常堆栈
        StackTraceElement[] stackTraceElements = ex.getStackTrace();
        // 通用配置
        OnelinkProperties.Common commonProp = this.onelinkProperties.getCommon();
        // 异常堆栈数量阈值
        int stackTraceThreshold = commonProp.getStackTraceThreshold();
        // 如果超过堆栈数量阈值,进行裁剪
        if (stackTraceElements != null && stackTraceThreshold > 0 && stackTraceElements.length > stackTraceThreshold) {
            StackTraceElement[] filteredStackTraces = StackTraceUtil.cutStackTrace(stackTraceElements, StackTraceUtil.DEFAULT_STACK_TRACE_CLASS_NAME_PREFIX_SET, false);
            ex.setStackTrace(filteredStackTraces);
            if (log.isDebugEnabled()) {
                log.debug("已自动裁剪掉{}个堆栈信息,原堆栈数量:{},当前堆栈数量:{}", stackTraceElements.length - filteredStackTraces.length, stackTraceElements.length, filteredStackTraces.length);
            }
        }
        // 如果开启等保异常详情屏蔽,或error为"-"则直接屏蔽掉所有异常详情
        if (commonProp.isMuteExceptionError() || StrUtil.DASHED.equals(resultVO.getError())) {
            resultVO.setError("");
            return;
        }
        // 异常堆栈详情
        StringBuilder errStackTraceSb = new StringBuilder("[").append(this.applicationName).append("] ");
        String stacktraceStr = ExceptionUtil.stacktraceToString(ex, -1);
        errStackTraceSb.append(StrUtil.blankToDefault(resultVO.getError(), ""))
                .append(stacktraceStr);
        resultVO.setError(errStackTraceSb.toString());
    }

    /**
     * 处理异常并转换为ResultVO
     */
    private ResultVO<Object> dispatchException(Exception ex) {
        // 内部RPC调用异常已在提供方处理过,直接返回即可
        if (ex instanceof InnerApiException) {
            return this.handleInnerApiException((InnerApiException) ex);
        }
        // 获取API异常
        ApiException apiException = this.resolveApiException(ex);
        if (apiException != null) {
            return this.handleApiException(apiException);
        }
        // 参数校验异常
        if (this.isValidationException(ex)) {
            return this.handleValidationException(ex);
            // 使用lombok @NonNull注解标记的参数验证
        } else if (ex instanceof NullPointerException && StrUtil.endWith(ex.getMessage(), LOMBOK_NONNULL_VALIDATION_MESSAGE_SUFFIX)) {
            String fieldName = StrUtil.subBefore(ex.getMessage(), " ", false);
            return this.handleApiException(ApiException.createEx(ExceptionCodeEnum.VALUE_NOT_NULL, fieldName));
            // 数据库相关异常
        } else if (ex instanceof SQLException) {
            return this.handleSqlException((SQLException) ex);
            // 第三方请求工具类异常
        } else if (ex instanceof HttpClientException) {
            return this.handleHttpClientException((HttpClientException) ex);
            // 无权限异常
        } else if (ex instanceof PermissionException) {
            return this.handlePermissionException((PermissionException) ex);
            // 请求参数绑定异常
        } else if (ex instanceof ServletRequestBindingException || ex instanceof TypeMismatchException) {
            this.response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        }

        // 二级异常处理
        ResultVO<Object> resultVO;
        Throwable cause = ex.getCause();
        // SQL相关异常
        if (cause instanceof SQLException) {
            resultVO = this.handleSqlException((SQLException) cause);
        } else {
            // 尝试处理第三方框架异常
            resultVO = this.resolveExternalEx(ex);
        }
        return resultVO;
    }

    /**
     * 处理内部服务调用异常
     */
    private ResultVO<Object> handleInnerApiException(InnerApiException ex) {
        // 远程调用如果是业务异常,返回200状态码
        if (Boolean.TRUE.equals(ex.getBizException())) {
            this.response.setStatus(HttpServletResponse.SC_OK);
        }
        return ResultVO.failure(ex.getMessage(), ex.getError()).setCode(ex.getCode());
    }

    /**
     * 处理HttpClient请求工具类异常
     */
    private ResultVO<Object> handleHttpClientException(HttpClientException ex) {
        String message;
        // TCP连接建立超时
        if (ex instanceof HttpClientConnectTimeoutException) {
            message = LocaleResolverUtil.getMessage(ExceptionCodeEnum.HTTP_CONNECT_TIME_OUT, ex.getConnectTimeoutMillis());
            // 请求超时
        } else if (ex instanceof HttpClientCallTimeoutException) {
            message = LocaleResolverUtil.getMessage(ExceptionCodeEnum.HTTP_CALL_TIME_OUT, ex.getCallTimeoutMillis());
            // 异常编码不为空说明是API异常
        } else if (ex.getExceptionCode() != null) {
            message = LocaleResolverUtil.getMessage(ex.getExceptionCode(), ex.getExceptionParams());
            // 非API异常默认使用HTTP异常编码
        } else {
            message = LocaleResolverUtil.getMessage(ExceptionCodeEnum.HTTP_ERROR);
        }
        return ResultVO.failure(message);
    }

    /**
     * 处理第三方框架异常 (例如 Dubbo RpcException , Feign ClientException)
     */
    private ResultVO<Object> resolveExternalEx(Throwable ex) {
        ResultVO<Object> resultVO = null;
        if (this.exceptionResolvers.size() > 0) {
            // 遍历所有异常解析器
            for (ExceptionResolver exceptionResolver : this.exceptionResolvers) {
                if (!exceptionResolver.couldResolve(ex)) {
                    continue;
                }
                // 异常解析器如果个性化处理了返回结果,则直接返回
                ResultVO<Object> resolvedResult = exceptionResolver.resolve(ex);
                if (resolvedResult != null) {
                    resultVO = resolvedResult;
                    break;
                }
            }
        }
        if (resultVO == null) {
            resultVO = ResultVO.failure(ex.getMessage());
        }
        return resultVO;
    }

    /**
     * 是否使用了@Valid进行参数验证
     */
    private boolean isValidationException(Exception ex) {
        // 使用@Valid且是@RequestBody方式入参
        return ex instanceof MethodArgumentNotValidException
                // 使用@Valid且是FormData方式入参
                || ex instanceof BindingResult
                // 非Controller使用@Valid
                || ex instanceof ConstraintViolationException
                // controller入参类型错误
                || ex instanceof MethodArgumentTypeMismatchException;
    }

    /**
     * 在非Controller接口中使用@Valid异常处理
     */
    private ResultVO<Object> handleValidationException(Exception ex) {
        this.response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        List<ValidationFieldInfo> fieldInfos;
        // 使用@Valid且是@RequestBody方式入参
        if (ex instanceof MethodArgumentNotValidException) {
            fieldInfos = this.handleMethodArgValidException(((MethodArgumentNotValidException) ex).getBindingResult());
            // 使用@Valid且是FormData方式入参
        } else if (ex instanceof BindingResult) {
            fieldInfos = this.handleMethodArgValidException((BindingResult) ex);
            // 非Controller使用@Valid
        } else if (ex instanceof ConstraintViolationException) {
            fieldInfos = handleConstraintViolationException((ConstraintViolationException) ex);
            // controller入参类型错误
        } else if (ex instanceof MethodArgumentTypeMismatchException) {
            MethodArgumentTypeMismatchException typeMismatchException = (MethodArgumentTypeMismatchException) ex;
            String fieldName = typeMismatchException.getName();
            ValidationFieldInfo fieldInfo = new ValidationFieldInfo()
                    .setFieldName(fieldName)
                    .setMessage(typeMismatchException.getMessage())
                    .setInvalidValue(typeMismatchException.getValue());
            fieldInfos = List.of(fieldInfo);
        } else {
            fieldInfos = List.of();
        }
        // 字段信息如果为空默认使用国际化异常信息进行返回
        if (fieldInfos.size() == 0) {
            String exMsg = LocaleResolverUtil.getMessage(ExceptionCodeEnum.REQ_ARG_NOT_VALID, ex.getMessage());
            return ResultVO.failure(ExceptionCodeEnum.REQ_ARG_NOT_VALID, exMsg, null);
        }
        // 是否翻译验证失败的字段名
        boolean isTranslateValidation = this.onelinkProperties.getWeb().isTranslateValidation();
        // 先使用Message方式,后续前端改造完成直接返回整个对象数据到ResultVO.data字段中 由前端自己拼接错误信息
        StringBuilder fieldErrorMsg = new StringBuilder();
        for (int i = 0; i < fieldInfos.size(); i++) {
            ValidationFieldInfo fieldInfo = fieldInfos.get(i);
            if (i != 0) {
                fieldErrorMsg.append(" , ");
            }
            String fieldName = isTranslateValidation ? StrUtil.blankToDefault(fieldInfo.getFieldDesc(), fieldInfo.getFieldName()) : fieldInfo.getFieldName();
            String message = LocaleResolverUtil.getMessage(fieldInfo.getMessage(), null);
            fieldErrorMsg.append("[").append(fieldName).append("] ").append(message);
        }
        String errMsg = LocaleResolverUtil.getMessage(ExceptionCodeEnum.REQ_ARG_NOT_VALID, fieldErrorMsg.toString());
        return ResultVO.failure(ExceptionCodeEnum.REQ_ARG_NOT_VALID, errMsg, null);
    }

    /**
     * 非Controller使用@Valid
     */
    private List<ValidationFieldInfo> handleConstraintViolationException(ConstraintViolationException ex) {
        List<ValidationFieldInfo> fieldInfos;
        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
        fieldInfos = constraintViolations.stream()
                .map(constraintViolation -> {
                    String paramClassName = constraintViolation.getLeafBean().getClass().getName();
                    String fieldName = StrUtil.subAfter(constraintViolation.getPropertyPath().toString(), ".", true);
                    String fieldDesc = this.validationFieldNameResolver.resolve(paramClassName, fieldName);
                    return new ValidationFieldInfo()
                            .setFieldName(fieldName)
                            .setMessage(constraintViolation.getMessage())
                            .setInvalidValue(constraintViolation.getInvalidValue())
                            .setFieldDesc(fieldDesc);
                })
                .collect(Collectors.toList());
        return fieldInfos;
    }

    private ResultVO<Object> handlePermissionException(PermissionException ex) {
        this.response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        ExceptionCode exceptionCode = ex.getExceptionCode();
        String message = LocaleResolverUtil.getMessage(exceptionCode, ex.getParams());
        return ResultVO.failure(exceptionCode, message, ex.getError());
    }

    /**
     * 使用@Valid且是@RequestBody方式入参
     */
    private List<ValidationFieldInfo> handleMethodArgValidException(BindingResult bindingResult) {
        String className = bindingResult.getTarget() == null ? null : bindingResult.getTarget().getClass().getName();
        return bindingResult.getFieldErrors().stream()
                .map(fieldError -> {
                    String fieldName = fieldError.getField();
                    String fieldDesc = this.validationFieldNameResolver.resolve(className, fieldName);
                    return new ValidationFieldInfo()
                            .setFieldName(fieldName)
                            .setMessage(fieldError.getDefaultMessage())
                            .setInvalidValue(fieldError.getRejectedValue())
                            .setFieldDesc(fieldDesc);
                }).collect(Collectors.toList());
    }

    /**
     * SQL异常处理
     */
    private ResultVO<Object> handleSqlException(SQLException sqlException) {
        ResultVO<Object> resultVO;
        String sqlExMsg = StrUtil.blankToDefault(sqlException.getMessage(), "");
        String exMsg;
        ExceptionCodeEnum respSqlCode;
        // ORACLE标准异常
        if (sqlExMsg.startsWith(ORACLE_EXCEPTION_PREFIX)) {
            // 使用原生数据库抛出的异常
            exMsg = sqlException.getMessage();
            respSqlCode = ExceptionCodeEnum.DB_SQL_ERROR;
            // SQL注入异常
        } else if (sqlExMsg.startsWith(SQL_INJECTION_EX_PREFIX)) {
            exMsg = sqlExMsg;
            respSqlCode = ExceptionCodeEnum.SQL_INJECTION;
            // 未知SQL异常
        } else {
            exMsg = sqlExMsg;
            respSqlCode = ExceptionCodeEnum.DB_SQL_ERROR;
        }
        resultVO = ResultVO.failure(respSqlCode, exMsg, null);
        return resultVO;
    }

    /**
     * API业务异常处理
     */
    private ResultVO<Object> handleApiException(ApiException apiException) {
        ExceptionCode exceptionCode = apiException.getExceptionCode();
        // 业务异常返回200状态码
        int httpStatus = exceptionCode instanceof OnelinkExceptionCode ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : HttpServletResponse.SC_OK;
        this.response.setStatus(httpStatus);
        String message;
        // 没有异常码默认设置为业务异常
        if (exceptionCode == null) {
            exceptionCode = ExceptionCodeEnum.BIZ_ERROR;
            message = MessageFormat.format(StrUtil.nullToEmpty(apiException.getMessage()), apiException.getParams());
        } else {
            message = LocaleResolverUtil.getMessage(exceptionCode, apiException.getParams());
        }
        apiException.setFormattedMessage(message);
        String error = StrUtil.format("[{}] {}", exceptionCode.getMessageKey(), StrUtil.nullToEmpty(apiException.getError()));
        return ResultVO.failure(exceptionCode, message, error);
    }

    /**
     * 获取API异常
     */
    private ApiException resolveApiException(Throwable ex) {
        // Api异常直接返回
        if (ex instanceof ApiException) {
            return (ApiException) ex;
        }
        // 一级嵌套异常
        Throwable cause = ex.getCause();
        if (cause == null) {
            return null;
        } else if (cause instanceof ApiException) {
            return (ApiException) cause;
        }
        // 二级嵌套异常
        Throwable secCause = cause.getCause();
        if (secCause instanceof ApiException) {
            return (ApiException) secCause;
        }
        // 三级嵌套以及更多级别的不做处理
        return null;
    }


}
